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());
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);
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)
};
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)
}
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); }
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),
};
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;
Summary
- NanoCore implements true 8-bit CPU emulation by using Rust's
u8wrapping arithmetic methods throughout the codebase. - The program counter and stack pointer use
wrapping_addandwrapping_subto handle address rollover at byte boundaries. - Arithmetic instructions leverage
overflowing_add,overflowing_sub, andoverflowing_multo 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →