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

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 and 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) 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) 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:

; 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, 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:

; 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:

; 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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →