6502 Emulator Runs 1 Instruction/S (Written in Markdown, Running in an LLM)

adunk1 pts0 comments

This 6502 Emulator Executes 1-3 Instructions Per Second (Written in Markdown, Running in an LLM) | Adam Dunkels

closing tag, Google Analytics can be placed here -->

ai,

programming,

just for fun,

This 6502 Emulator Executes 1-3 Instructions Per Second (Written in Markdown, Running in an LLM)

Adam Dunkels, PhD Follow<br>May 25, 2026 ·

18 mins read

Share this

In previous posts we explored what happens if we treat LLMs as processors that run Markdown as their machine code: a user-space IP stack written in Markdown and a BASIC interpreter written in Markdown. Today we are going to look at an emulator for a 6502 microprocessor written in Markdown and executed with OpenCode and the GLM 5.1 model running on Grunden.ai.

Yes, this is a ridiculous idea with no practical value. But it is a fun experiment.

A 6502 Microprocessor Written in Markdown

The 6502 microprocessor was a popular microprocessor for home computers in the 1980s. It was used in the Commodore 64, Apple II, Nintendo Entertainment System , among many others.

The 6502 microprocessor has three built-in registers (A, X, and Y) and can address a memory range of 64k. In addition to the three registers, it also has a 256 byte memory bank, called zero-page, that works like a larger register pool. The stack is fixed at address $0100 and limited to 256 bytes.

Opcodes are one byte wide and take zero, one, or two additional bytes as arguments. There are several undocumented opcodes because the opcode decoder circuitry was simplified. But for now, we do not worry about those.

Let’s see what happens if we implement a 6502 emulator in Markdown, with the documented instructions and addressing modes (except the weird Binary Coded Decimal mode). This is the resulting code:

run-6502.md (click to expand)

# Run 6502 — LLM as CPU

You are a MOS 6502 CPU emulator. The machine code is provided inline below as hex bytes. Execute it by fetching opcodes, decoding instructions, computing results, tracking registers/flags/memory, and following control flow — all in your own reasoning. No libraries, no Python, no calculator tools.

## Program

``<br>$ARGUMENTS<br>``

## Memory Model

- 64KB address space ($0000–$FFFF), sparsely tracked (only store bytes that are written).<br>- Program is loaded starting at **$0600** (the first byte in the hex dump is at $0600).<br>- Output region: **$0200–$02FF**. After execution, this region is displayed as the program's output.<br>- Stack: **$0100–$01FF**. Stack pointer (SP) indexes into this page.<br>- Zero page: **$0000–$00FF**. Fast access, used by zero-page addressing modes.

## CPU State

Initialize before execution:

``<br>Registers:<br>A = $00 (accumulator, 8-bit)<br>X = $00 (X index register, 8-bit)<br>Y = $00 (Y index register, 8-bit)<br>SP = $FD (stack pointer, 8-bit, points into $01xx)<br>PC = $0600 (program counter, 16-bit)

Status flags (P register):<br>N = 0 (Negative: bit 7 of result)<br>V = 0 (Overflow: signed overflow on ADC/SBC)<br>B = 0 (Break: set by BRK)<br>I = 0 (Interrupt disable)<br>Z = 0 (Zero: result is zero)<br>C = 0 (Carry: unsigned overflow on ADC, unsigned borrow on SBC)

Memory: (empty — only the program bytes are loaded)<br>``

## Fetch-Decode-Execute Loop

Repeat until halted (BRK encountered or PC runs past loaded program bytes):

1. **Fetch**: Read the byte at PC. This is the opcode.<br>2. **Decode**: Look up the opcode in the instruction table below. Determine the mnemonic, addressing mode, and byte count.<br>3. **Read operands**: Fetch additional bytes as required by the addressing mode.<br>4. **Execute**: Perform the operation. Update registers, flags, and memory as specified.<br>5. **Advance PC**: PC += instruction byte count (already done during fetch/operand read).

**After every instruction**, track state in your reasoning:

``<br>[$xxxx] MNEMONIC operand → A=$xx X=$xx Y=$xx SP=$xx | NV-BDIZC=xxxxxxxx | PC=$xxxx<br>``

This is mandatory. It catches errors in flag computation and addressing.

## Addressing Modes

| Mode | Syntax | Bytes | How to resolve |<br>|------|--------|-------|----------------|<br>| Implied | `CLC` | 1 | No operand |<br>| Immediate | `LDA #$xx` | 2 | Value is the byte after opcode |<br>| Zero Page | `LDA $xx` | 2 | Address is $00xx; read/write that byte |<br>| Zero Page,X | `LDA $xx,X` | 2 | Address is ($xx + X) & $FF; read/write that byte |<br>| Zero Page,Y | `LDX $xx,Y` | 2 | Address is ($xx + Y) & $FF; read/write that byte |<br>| Absolute | `LDA $xxxx` | 3 | Address is the 16-bit value (low byte first); read/write that byte |<br>| Absolute,X | `LDA $xxxx,X` | 3 | Address is (16-bit value + X) & $FFFF |<br>| Absolute,Y | `LDA $xxxx,Y` | 3 | Address is (16-bit value + Y) & $FFFF |<br>| Relative | `BEQ $xx` | 2 | Signed offset (-128 to +127) added to PC (after PC has advanced past this instruction) |

## Instruction Set

### Load/Store

| Opcode | Mnemonic | Mode | Flags |<br>|--------|----------|------|-------|<br>| $A9 | LDA #imm | Immediate | N, Z |<br>| $A5 | LDA zp | Zero Page | N, Z |<br>| $B5 | LDA zp,X | Zero Page,X | N, Z |<br>| $AD | LDA abs | Absolute | N, Z |<br>| $BD | LDA...

byte zero markdown address page written

Related Articles