index

pg_6502

a boring monday morning that started off the way i didn’t want it to. last night Ben Eater’s 65c02 playlist plopped up on my youtube home page. this was right after my friend and i were talking about how fun absurd projects are to work on, like discodb. i love going on twitter posting about another weird thing i did with sql and seeing folks call me insane. this tweet was a huge benefactor in increasing the time i spend on these projects.

so yeah. i ended up building a MOS Tech 6502 emulator in pure postgres.

the core idea

the whole machine lives in two tables:

  • pg6502.cpu: one row for a, x, y, sp, pc, plus flags
  • pg6502.mem: 64KB address space, one row per byte (addr -> val)

every instruction is a stored procedure. execution consists of the following steps:

  1. read opcode from mempc
  2. decode via opcode_table
  3. dispatch to op_* function
  4. update cpu state and memory
  5. repeat until BRK

how i built it

i built it in layers because otherwise this thing would be impossible to debug:

  1. schema and memory primitives (mem_read, mem_write, mem_read16)
  2. addressing mode functions (IMM, ZP, ABS, IND, etc.)
  3. flag helpers (flags_to_byte, byte_to_flags, set_nz)
  4. opcode table + opcode implementations (LDA, ADC, branches, stack ops, shifts/rotates, etc.)
  5. execution engine (execute_instruction, run, reset, loader, state view)
  6. tiny assembler inside postgres so tests can use assembly text directly. i did this because it looked better as a tweet i won’t lie.

04_opcodes.sql got pretty big pretty fast, but keeping each opcode explicit made behavior easier to reason about.

breaks and patches

once i was done implementing the architecture of the chip, it was time to start testing everything after 4 hours of hand-writing SQL with no LLMs to help me. all i did was stare at docs related to 6502 and write SQL.

1. flag updates were wrong

at one point set_nz logic was flipped. everything kinda worked until it really didn’t. branch behavior had started drifting in ways that looked random. i ended up tracing flag transitions step by step.

the fix was really simple, i had to correct Z & N updates.

2. vector/address constants bit me

i briefly used 16#FFFC style constants. postgres did not find that funny. hard crowd, happens. the reset/interrupt vectors need plain decimal addresses (65532, 65534). once i fixed that, reset and BRK behavior lined up just fine.

3. missing addressing edge cases

some zero-page X/Y paths were missing or mismatched. that was silently pointing reads/writes to wrong addresses, which then made later instructions break. i added the missing mode coverage and corresponding opcode mappings and that got fixed too.

4. assembler + fibonacci bugs

the assembler initially handled only part of operand parsing cleanly. indexed and absolute-vs-zero-page decisions needed better parsing logic. branch offsets in the fibonacci loop were also wrong in an earlier version.

fixed the parser rules for X/,Y, zero-page vs absolute selection. had to change code in the fib program and some assertions. once done, it was working and i had done it. i dropped my girlfriend an update (love that i can just tell her these things and she gets it). and a bit later tweeted about it. that did pretty good too.

testing strategy

i’m not going to say this is a full conformance suite. i ran targeted SQL tests:

  • load program via pg6502.assemble(...)
  • reset()
  • run(max_cycles)
  • assert memory/register outcomes

the fibonacci test is the main smoke test. it validates expected values at $20/$21/$22 and throws if they don’t match, so regressions fail loudly instead of “looking fine.”

closing

i learnt that postgres is so much more programmable than i thought. i love these weird projects i get to work on. and this one has only made me more excited to work on what’s coming up.