# How to Implement Common Loop Structures in NanoCore Assembly: 3 Complete Examples

> Master NanoCore assembly loops using Zero flag and conditional jumps. Explore 3 complete examples to control iteration flow efficiently in your 8-bit projects.

- Repository: [Afaan Bilal/nanocore](https://github.com/afaanbilal/nanocore)
- Tags: how-to-guide
- Published: 2026-02-23

---

**NanoCore loops rely on the Zero flag (Z), decrement instructions, and conditional jumps like `JNZ` and `JZ` to control iteration flow in this 8-bit von Neumann architecture.**

NanoCore is a lightweight 8-bit CPU emulator with a classic von Neumann design. To implement common loop structures in NanoCore assembly, developers utilize a minimal set of registers, arithmetic flags, and jump instructions. This guide examines three production-ready examples from the `afaanbilal/nanocore` repository that demonstrate counted loops, comparison-based iteration, and nested control flow.

## Architecture Foundations for Loop Control

Before writing loops, understand the hardware primitives defined in [`src/lib.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/lib.rs) and [`src/cpu.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/cpu.rs).

### Registers and Flags

The CPU provides **16 general-purpose registers (R0-R15)** for holding counters and temporary values. The **Zero flag (Z)**—set via `CPU::set_flag(CPU::FLAG_Z)` when arithmetic results equal zero—drives all conditional branching. The **Program Counter (PC)** tracks execution flow and updates when jump instructions modify the instruction pointer.

### Jump Instructions

The instruction set (`enum Op` in [`src/lib.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/lib.rs)) provides several control flow opcodes:

- **Unconditional jumps**: `JMP`, `JMPR`, `CALL`, `RET`
- **Conditional jumps**: `JZ` (jump if zero) and `JNZ` (jump if not zero)

The assembler (`Assembler::map_labels` in [`src/assembler.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/assembler.rs)) resolves textual labels like `loop:` into concrete byte offsets. When emitting `JNZ` (opcode `0x18`), the assembler calculates the relative offset to the target label.

## Counted Loops with Decrement-Until-Zero

The most efficient pattern uses **DEC** to update counters and **JNZ** to branch while the Zero flag remains cleared.

### Example: Fibonacci Sequence Printer

This pattern appears in `programs/fib.nca`, printing the first seven Fibonacci numbers:

```nca
; Print the fibonacci sequence (first 7)
start:
    LDI R0 0          ; first term
    LDI R1 1          ; second term
    LDI R2 7          ; loop counter
loop:
    LDI R4 48         ; ASCII '0'
    ADD R4 R0         ; convert digit to char
    PRINT R4

    MOV R3 R1
    ADD R1 R0
    MOV R0 R3
    DEC R2            ; decrement counter, sets Z when R2 == 0
    JNZ loop          ; repeat while counter != 0
end:
    HLT

```

The `DEC R2` instruction automatically sets the **Zero flag** when the register reaches zero. The `JNZ loop` instruction checks this flag; as implemented in [`src/cpu.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/cpu.rs), the branch occurs only when `CPU::FLAG_Z` is cleared.

## Increment-Based Loops with Explicit Comparison

When iterating upward or comparing against non-zero terminating values, use **SUB** to set the Zero flag explicitly.

### Example: ASCII Character Printer

The `programs/abcde.nca` file demonstrates iterating from 'A' to 'E' using explicit comparison:

```nca
; Prints ABCDE
start:
    LDI R0 65         ; ASCII 'A'
loop:
    PRINT R0
    INC R0            ; next character
    LDI R1 70         ; ASCII after 'E'
    SUB R1 R0         ; compare: sets Z when R0 == 70
    JNZ loop          ; continue until values match
    HLT

```

Here, `SUB R1 R0` performs the comparison by subtraction. When `R0` reaches 70, the result is zero, triggering `CPU::update_zn_flags` to set the Z-flag and causing `JNZ` to fall through to the next instruction.

## Implementing Nested Loop Structures

Complex programs require multiple levels of iteration. NanoCore supports this through stacked counters and mixed conditional/unconditional jumps.

### Example: Rectangle Drawing Program

The `programs/draw_rectangle.nca` implementation shows nested loops controlling width and height independently:

```nca
; Draw a rectangle
start:
    LDI R10 10        ; newline
    LDI R11 32        ; space
    LDI R12 35        ; '#'
    LDI R2 20         ; width
    LDI R3 6          ; height
    LDI R4 2

print_border_line:
    PRINT R12
    DEC R2
    JNZ print_border_line   ; inner loop: top/bottom edge
    DEC R4
    JZ end
    PRINT R10

print_line:
    PRINT R12
    LDI R2 18
print_line_space:
    PRINT R11
    DEC R2
    JNZ print_line_space    ; inner loop: fill interior spaces
    PRINT R12
    PRINT R10
    DEC R3
    JNZ print_line          ; outer loop: next row
    LDI R2 20
    JMP print_border_line   ; unconditional jump to draw bottom
end:
    HLT

```

This example demonstrates three distinct loop constructs:

- **Inner width loops** using `DEC`/`JNZ` pairs (`print_border_line` and `print_line_space`)
- **Outer height loop** decrementing `R3` and branching with `JNZ print_line`
- **Unconditional transition** using `JMP` to move between distinct loop phases

## Summary

Implementing loops in NanoCore assembly requires understanding the interaction between arithmetic instructions and the Zero flag:

- **Initialize counters** in registers R0-R15 using `LDI`
- **Update state** with `DEC` (automatically sets Z at zero) or `SUB` (explicit comparison)
- **Branch conditionally** using `JNZ` to continue iteration or `JZ` to exit
- **Resolve labels** through the assembler's `map_labels` function, which converts symbolic names to byte offsets for opcodes like `0x18` (`JNZ`)

These primitives enable everything from simple counted iterations to complex nested geometries.

## Frequently Asked Questions

### How does the Zero flag get set in NanoCore assembly?

The Zero flag is set automatically by arithmetic operations that result in zero. Specifically, [`src/cpu.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/cpu.rs) implements `CPU::update_zn_flags` and `CPU::set_flag(CPU::FLAG_Z)` to mark the flag when instructions like `DEC`, `SUB`, or `ADD` produce a zero result. Conditional jumps `JZ` and `JNZ` inspect this flag to determine branching behavior.

### What is the difference between JMP and JNZ in NanoCore?

`JMP` (defined in [`src/lib.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/lib.rs) as `Op::JMP`) performs an unconditional jump to the target label, always altering the Program Counter. `JNZ` (opcode `0x18`, `Op::JNZ`) is conditional—it only jumps if the Zero flag is cleared (meaning the last operation produced a non-zero result). Use `JMP` for infinite loops or state transitions, and `JNZ` for counted iterations.

### Can I use registers other than R0 for loop counters?

Yes. NanoCore provides **16 general-purpose registers (R0-R15)**, and any register can serve as a loop counter. The examples in `programs/fib.nca` use `R2`, while `programs/draw_rectangle.nca` utilizes `R2`, `R3`, and `R4` simultaneously for different loop dimensions. Choose registers based on availability and whether you need to preserve values across nested scopes.

### How does the assembler handle loop labels?

The `Assembler::map_labels` function in [`src/assembler.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/assembler.rs) parses labels ending with `:` (like `loop:`) during the first pass, recording their byte offsets. During the second pass, it substitutes label references in jump instructions with calculated relative offsets. This allows `JNZ loop` to emit the correct machine code pointing to the label's memory location.