How the PRINT Instruction Outputs Characters and Strings to the Console in NanoCore
The PRINT instruction in NanoCore reads a single byte from a specified register, converts it to an ASCII character, and appends it to an internal output buffer while optionally echoing the character to standard output in interactive mode.
The PRINT instruction serves as the primary I/O mechanism in NanoCore, a lightweight Rust-based emulator and assembler for educational purposes. This one-operand opcode enables assembly programs to communicate results by translating register values into human-readable text. To understand how the PRINT instruction outputs characters and strings to the console, we must trace its implementation from opcode definition through assembly parsing to runtime execution.
Architecture of the PRINT Instruction
The PRINT instruction traverses a three-stage pipeline: opcode definition, assembly encoding, and runtime execution. Each stage is implemented in a specific source file within the NanoCore repository.
Opcode Definition in src/lib.rs
In src/lib.rs, the Op::PRINT variant is defined as a single-operand instruction occupying exactly 2 bytes—one byte for the opcode and one byte for the register operand. The instruction reports a length of 2 bytes to the decoder, ensuring the emulator correctly calculates the next instruction address during the fetch-decode-execute cycle.
Assembly Parsing in src/assembler.rs
The assembler, located in src/assembler.rs, recognizes the PRINT Rx syntax during the parsing phase. It validates that exactly one register argument is provided, then emits the opcode byte followed by the encoded register index. This transformation converts the human-readable assembly mnemonic into the machine code format expected by the emulator.
Runtime Execution in src/nanocore.rs
The actual character output logic resides in src/nanocore.rs within the emulator's main execution loop. When the CPU encounters Op::PRINT, the emulator performs the following sequence:
- Validates that the operand is a
Regtype - Retrieves the byte value from
self.cpu.registers[reg as usize] - Appends the value to
self.output, an internalStringbuffer that accumulates all printed characters - If
self.printistrue(interactive mode), executesprint!("{}", value as char)to write the character to standard output
This dual-channel approach ensures that output is always captured for programmatic inspection while providing immediate console feedback during interactive sessions.
Writing Assembly Programs with PRINT
NanoCore supports both manual character-by-character printing and automated string expansion through pseudo-directives.
Printing Individual Characters
To output text manually, load ASCII values into registers and invoke PRINT for each byte:
; Load the ASCII codes for 'H', 'i', and '!' into registers
LDI R0 72 ; 'H'
LDI R1 105 ; 'i'
LDI R2 33 ; '!'
; Print each character
PRINT R0
PRINT R1
PRINT R2
HLT
Assembling and running this program:
cargo run --bin nca programs/hello.nca
Output:
Hi!
Each PRINT instruction reads the register's byte value, converts it to a character, and writes it to both the internal buffer and the console.
Using the .STRING Pseudo-Directive
The assembler provides a .STRING "text" directive that expands into a series of PRINT instructions automatically:
.STRING "Hello, world!"
HLT
Under the hood, the assembler converts the string literal into multiple PRINT operations, one per character, utilizing the same execution path in src/nanocore.rs.
Programmatic Usage in Rust
When using NanoCore as a library, you can capture output programmatically:
use nanocore::{Assembler, NanoCore};
let source = "\
LDI R0 65 ; 'A'
PRINT R0
HLT";
let mut asm = Assembler::new(source);
asm.assemble().unwrap();
let mut core = NanoCore::new();
core.run(&asm.program).unwrap();
assert_eq!(core.output(), "A"); // The internal buffer contains the printed character
Internal Buffer vs Console Output
NanoCore maintains separate channels for output persistence and display. The self.output field always accumulates printed characters into a String, enabling post-execution inspection. Conversely, the print!() macro only executes when self.print is enabled, providing immediate visual feedback without polluting stdout during automated testing.
Summary
- The PRINT instruction is a 2-byte opcode (
Op::PRINT) defined insrc/lib.rsthat accepts a single register operand - During execution in
src/nanocore.rs, it reads one byte from the specified register and converts it to acharusing ASCII encoding - Output is always stored in the internal
self.outputbuffer and optionally printed to stdout whenself.printistrue - The assembler in
src/assembler.rsparses both explicitPRINT Rxinstructions and the.STRINGpseudo-directive - Programs output strings by executing multiple PRINT instructions, either manually coded or generated via the
.STRINGdirective
Frequently Asked Questions
What register size does the PRINT instruction use?
The PRINT instruction reads exactly one byte (8 bits) from the specified register. It treats this value as an ASCII character code, allowing direct output of standard ASCII characters (0-127). The emulator converts this byte to a Rust char type during the print!() call.
Can PRINT output Unicode or multi-byte characters?
No, the PRINT instruction handles only single-byte ASCII values. To output multi-byte UTF-8 characters, you must manually split the byte sequence across multiple registers and execute separate PRINT instructions for each byte. Alternatively, use the .STRING directive, which handles the byte expansion automatically while still emitting individual PRINT operations for each byte.
How do I capture PRINT output in my Rust code when using NanoCore as a library?
After executing core.run(), call the output() method on your NanoCore instance. This method returns the contents of the internal self.output buffer as a String, containing every character printed during execution regardless of whether console output was enabled. This design facilitates unit testing without requiring stdout capture.
What is the difference between the internal output buffer and console output?
The internal buffer (self.output) permanently stores all printed characters for later retrieval via the API, while console output (via print!()) only occurs when the emulator runs in interactive mode (self.print == true). This separation allows test suites to verify program output silently while enabling interactive sessions to provide immediate visual feedback to users.
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 →