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 fora,x,y,sp,pc, plus flagspg6502.mem: 64KB address space, one row per byte (addr -> val)
every instruction is a stored procedure. execution consists of the following steps:
- read opcode from
mempc - decode via
opcode_table - dispatch to op_* function
- update cpu state and memory
- repeat until
BRK
how i built it
i built it in layers because otherwise this thing would be impossible to debug:
- schema and memory primitives (
mem_read,mem_write,mem_read16) - addressing mode functions (
IMM,ZP,ABS,IND, etc.) - flag helpers (
flags_to_byte,byte_to_flags,set_nz) - opcode table + opcode implementations (
LDA,ADC, branches, stack ops, shifts/rotates, etc.) - execution engine (execute_instruction, run, reset, loader, state view)
- 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.