How NanoCore's Wrapping Arithmetic Manages Integer Overflow and Underflow

NanoCore uses Rust's built-in wrapping_* and overflowing_* methods on u8 types to emulate true 8-bit CPU behavior, automatically wrapping values modulo 256 while setting the carry flag for overflow detection.

NanoCore is a Rust-based emulator for a true 8-bit CPU architecture where every register, the program counter (PC), and the stack pointer (SP) are strictly u8 values. In this constrained environment, NanoCore's wrapping arithmetic ensures that any operation exceeding the 0–255 range wraps around seamlessly, matching hardware behavior exactly.

Understanding 8-Bit Wrapping Arithmetic in NanoCore

In an authentic 8-bit processor, arithmetic operations do not panic or throw exceptions when exceeding byte limits. Instead, values wrap using modulo 256 arithmetic. NanoCore implements this behavior by leveraging Rust's wrapping arithmetic primitives on unsigned 8-bit integers, ensuring that 0xFF + 1 becomes 0x00 and 0x00 - 1 becomes 0xFF without runtime overhead.

How NanoCore Implements Wrapping for PC and SP

Program Counter Wrapping with wrapping_add

The program counter advances after each instruction using wrapping_add, allowing execution to flow from address 0xFF directly to 0x00, essential for circular memory layouts.

// src/nanocore.rs – updating PC when no explicit jump occurs
self.cpu.pc = self.cpu.pc.wrapping_add(op.instruction_len());

View source

Stack Pointer Management with Bounds Checking

Push and pop operations modify the stack pointer using wrapping arithmetic, but NanoCore first validates against STACK_MIN and STACK_MAX to raise explicit overflow/underflow errors before the wrap occurs.

// src/nanocore.rs – PUSH
self.cpu.sp = self.cpu.sp.wrapping_sub(1);

// src/nanocore.rs – POP
self.cpu.sp = self.cpu.sp.wrapping_add(1);

Push source | Pop source

Register Operations: INC and DEC

The increment and decrement instructions use wrapping arithmetic to ensure seamless rollover at byte boundaries.

// src/nanocore.rs – INC / DEC
let value = if op == Op::INC {
    self.cpu.registers[reg as usize].wrapping_add(1)
} else {
    self.cpu.registers[reg as usize].wrapping_sub(1)
};

View source

Arithmetic Instructions and Overflow Detection

Using overflowing_* Methods for ADD, SUB, MUL

NanoCore's execute_arithmetic function in src/nanocore.rs utilizes Rust's overflowing_add, overflowing_sub, and overflowing_mul methods. These return a tuple containing the wrapped result and a boolean indicating whether overflow occurred.

fn execute_arithmetic(&self, op: Op, v1: u8, v2: u8) -> crate::EmulatorResult<(u8, bool)> {
    let result = match op {
        Op::ADD | Op::ADDI => v1.overflowing_add(v2),
        Op::SUB | Op::SUBI => v1.overflowing_sub(v2),
        Op::MUL | Op::MULI => v1.overflowing_mul(v2),
        // DIV / MOD use overflowing_div / overflowing_rem after a zero-check

    };
    Ok(result)
}

Source

Setting the Carry Flag (FLAG_C)

After arithmetic execution, NanoCore updates the destination register with the wrapped result, adjusts the Zero and Negative flags via update_zn_flags, and conditionally sets the carry flag (FLAG_C) based on the overflow boolean.

self.cpu.registers[rd as usize] = result;
self.cpu.update_zn_flags(result);
if carry { self.cpu.set_flag(CPU::FLAG_C); } else { self.cpu.clear_flag(CPU::FLAG_C); }

Source

Bitwise Operations: Shifts and Rotates

Shift operations use overflowing_shl and overflowing_shr to capture the bit shifted out into the carry flag. Rotate operations manually extract the bit that wraps around to set FLAG_C appropriately.

let (result, carry) = match op {
    Op::SHL => value.overflowing_shl(1),
    Op::SHR => value.overflowing_shr(1),
    Op::ROL => (value.rotate_left(1), (value & 0x80) != 0),
    Op::ROR => (value.rotate_right(1), (value & 0x01) != 0),
};

Source

Memory Addressing with Wrapping

When loading programs into the emulated 256-byte memory space, NanoCore uses wrapping_add on the start address. This permits programs that cross the 0xFF → 0x00 boundary without manual bounds checks, as the earlier size validation already guarantees the program fits within memory.

self.cpu.memory[start_address.wrapping_add(i as u8) as usize] = byte;

Source

Summary

  • NanoCore implements true 8-bit CPU emulation by using Rust's u8 wrapping arithmetic methods throughout the codebase.
  • The program counter and stack pointer use wrapping_add and wrapping_sub to handle address rollover at byte boundaries.
  • Arithmetic instructions leverage overflowing_add, overflowing_sub, and overflowing_mul to obtain both the wrapped result and the carry bit for flag updates.
  • The carry flag (FLAG_C) accurately reflects overflow conditions from arithmetic, shifts, and rotates, matching hardware behavior.
  • Memory addressing uses wrapping addition to support programs that cross the 0xFF boundary seamlessly.

Frequently Asked Questions

What happens when the program counter reaches 0xFF in NanoCore?

When the program counter reaches 0xFF and the next instruction requires incrementing, NanoCore uses wrapping_add to advance the PC. This causes the counter to roll over from 0xFF to 0x00, accurately emulating the circular memory addressing of real 8-bit processors.

How does NanoCore detect arithmetic overflow?

NanoCore detects overflow using Rust's overflowing_* methods (such as overflowing_add and overflowing_sub). These methods return a tuple containing the arithmetic result and a boolean indicating whether overflow occurred. The emulator then sets or clears the carry flag (FLAG_C) based on this boolean value.

Why does NanoCore use wrapping arithmetic instead of panicking on overflow?

NanoCore emulates a true 8-bit CPU where hardware registers naturally wrap around at byte boundaries (modulo 256). In real 8-bit processors, overflow does not cause crashes but instead produces defined wraparound behavior with appropriate flag settings. Using Rust's wrapping arithmetic allows NanoCore to match this hardware behavior exactly without runtime overhead.

How is the stack pointer protected from overflow and underflow?

While the stack pointer uses wrapping_add and wrapping_sub for arithmetic operations, NanoCore explicitly validates the SP against STACK_MIN and STACK_MAX constants before performing push or pop operations. This validation raises explicit overflow or underflow errors if the stack would exceed its defined boundaries, preventing silent corruption while still allowing the pointer itself to wrap if needed.

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 →