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) andJNZ(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/JNZpairs (print_border_lineandprint_line_space) - Outer height loop decrementing
R3and branching withJNZ print_line - Unconditional transition using
JMPto 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) orSUB(explicit comparison) - Branch conditionally using
JNZto continue iteration orJZto exit - Resolve labels through the assembler's
map_labelsfunction, which converts symbolic names to byte offsets for opcodes like0x18(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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →