Practical Differences Between JMPR and JMP Instructions in NanoCore

JMP uses an immediate address encoded directly in the instruction, while JMPR reads the target address from a register at runtime, enabling dynamic control flow such as jump tables and function pointers.

In the NanoCore emulator, both JMP and JMPR serve as unconditional jump instructions that alter program flow by modifying the program counter. However, their distinct addressing modes determine when the target address is resolved and what coding patterns they support. Understanding these practical differences is essential for writing efficient assembly for this educational CPU architecture.

Operand Encoding and Instruction Format

Both instructions occupy 2 bytes (opcode plus operand) as defined in src/lib.rs, but they interpret their operands differently.

JMP: Immediate Addressing

The JMP instruction maps to opcode 0x24 and expects an Addr operand—a 16-bit immediate value representing the absolute target address. During assembly, the label or address is encoded directly into the instruction bytes.

// From src/lib.rs - Op enum definition
JMP = 0x24,  // Takes Addr operand

JMPR: Register Indirect Addressing

The JMPR instruction uses opcode 0x25 and expects a Reg operand. The actual jump target is not encoded in the instruction; instead, the CPU reads the value stored in the specified register at execution time and treats that value as the destination address.

// From src/lib.rs - Op enum definition
JMPR = 0x25, // Takes Reg operand

Runtime Execution in the NanoCore Emulator

The execution engine in src/nanocore.rs handles these instructions through distinct code paths that demonstrate their operational differences.

JMP Execution Path: When executing JMP, the emulator extracts the immediate address from the instruction and writes it directly to the program counter:

// From src/nanocore.rs lines 86-100
Op::JMP => {
    let a = self.next_addr(); // Reads immediate 16-bit address
    self.cpu.pc = a;          // Direct assignment to PC
    pc_override = true;
}

JMPR Execution Path: When executing JMPR, the emulator first reads the register value, then uses that value as the target address:

// From src/nanocore.rs lines 101-115
Op::JMPR => {
    let reg = self.next_reg();                    // Reads register index
    let addr = self.cpu.registers[reg as usize];  // Dereferences register
    self.cpu.pc = addr;                           // Jump to address stored in register
    pc_override = true;
}

Practical Use Cases and Control Flow Patterns

The distinction between immediate and register-indirect addressing creates fundamentally different programming patterns.

Static Jumps with JMP

Use JMP for control flow where the destination is known at assembly time:

  • Loops: Jumping back to a fixed label at the start of a loop body
  • Unconditional forward jumps: Skipping over code sections
  • Fixed entry points: Jumping to known subroutine addresses
; Count from 0 to 4 using JMP for loop control
LDI R0 0        ; R0 = 0
LOOP:
    PRINT R0    ; Output current value
    ADDI R0 1   ; Increment
    SUBI R0 5   ; Compare with 5
    JZ END      ; Exit if zero
    JMP LOOP    ; Static jump back to LOOP
END:
    HLT

Dynamic Dispatch with JMPR

Use JMPR when the target address is computed at runtime or stored in memory:

  • Jump tables: Implementing switch-case statements by indexing into a table of addresses
  • Function pointers: Calling dynamically assigned subroutines
  • Computed gotos: Jumping to addresses calculated from input data
  • Return from subroutine: When paired with CALLR, jumping to the address saved in a register
; Jump table example: dispatch based on selector in R1
LDI R0 0        ; Address of handler0
LDI R2 10       ; Address of handler1  
LDI R3 20       ; Address of handler2
LDI R1 1        ; Selector = 1 (choose handler1)

; Build jump table at memory address 0x30
LDI R4 0x30
STORE R0 R4     ; table[0] = handler0
INC R4
STORE R2 R4     ; table[1] = handler1
INC R4
STORE R3 R4     ; table[2] = handler2

; Load selected handler address
LDI R5 0x30     ; Base of table
ADD R5 R1       ; R5 points to selected entry
LDR R6 R5       ; R6 = address from table
JMPR R6         ; Indirect jump to chosen handler

handler0:
    PRINT R0
    JMP END
handler1:
    PRINT R2
    JMP END
handler2:
    PRINT R3
    JMP END
END:
    HLT

Summary

  • JMP encodes the target address as an immediate 2-byte value, making it ideal for static control flow like loops and fixed subroutine jumps.
  • JMPR reads the target address from a register, enabling dynamic dispatch through jump tables, function pointers, and computed control flow.
  • Both instructions occupy 2 bytes and override the program counter, but JMPR adds a register read operation during execution.
  • In the NanoCore source code, these differences are implemented in src/nanocore.rs with distinct execution paths for opcode 0x24 (JMP) and 0x25 (JMPR).

Frequently Asked Questions

Can JMPR jump to any address in memory?

Yes, JMPR can jump to any 16-bit address stored in a register. The register value is treated as an absolute address and written directly to the program counter. This allows jumping to addresses computed at runtime or loaded from memory tables.

Is JMP faster than JMPR in the NanoCore emulator?

In the current NanoCore implementation, both instructions execute in similar time complexity, but JMPR requires one additional memory access to read the register value. JMP extracts the address directly from the instruction stream via next_addr(), while JMPR calls next_reg() and then indexes into self.cpu.registers[].

When should I use JMP instead of JMPR?

Use JMP when the target address is known at assembly time, such as for loops, fixed subroutine calls, or skipping code blocks. Use JMPR when the target depends on runtime data, such as implementing switch statements via jump tables, calling function pointers, or returning from subroutines where the return address is stored in a register.

Does JMPR modify the register containing the jump address?

No, JMPR only reads the register value. The register contents remain unchanged after the jump executes. This allows the same register to be reused for subsequent operations or to store the address for later use.

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 →