Hay proyectos que nacen de la necesidad. Otros, de la curiosidad enfermiza. Y luego está pg_6502: un emulador completo del microprocesador MOS 6502 —el cerebro del NES, el Commodore 64, la Apple II— escrito enteramente en SQL dentro de PostgreSQL.
No es una broma. No es un April Fools. Es PL/pgSQL puro ejecutando código máquina de 8 bits dentro de tu base de datos favorita.
¿Qué es el MOS 6502 y por qué importa?
Si alguna vez jugaste al Super Mario Bros o programaste en un Commodore 64, el 6502 fue el responsable. Es un procesador de 8 bits con un set de instrucciones minimalista pero elegante: acumulador, dos registros índice (X e Y), un stack pointer de 256 bytes, y un program counter de 16 bits que direcciona 64KB de memoria.
Su belleza está en la simplicidad. 56 instrucciones, 13 modos de direccionamiento, y un diseño tan limpio que todavía se estudia en cursos de arquitectura de computadores.
La arquitectura de pg_6502: SQL como silicona
El proyecto, publicado por lasect en GitHub, convierte cada concepto del hardware en primitivas SQL:
Los registros del CPU son una sola fila:
CREATE TABLE pg6502.cpu (
a INT NOT NULL DEFAULT 0 CHECK (a BETWEEN 0 AND 255),
x INT NOT NULL DEFAULT 0 CHECK (x BETWEEN 0 AND 255),
y INT NOT NULL DEFAULT 0 CHECK (y BETWEEN 0 AND 255),
sp INT NOT NULL DEFAULT 255 CHECK (sp BETWEEN 0 AND 255),
pc INT NOT NULL DEFAULT 0 CHECK (pc BETWEEN 0 AND 65535),
flag_n BOOL NOT NULL DEFAULT FALSE,
flag_v BOOL NOT NULL DEFAULT FALSE,
flag_b BOOL NOT NULL DEFAULT FALSE,
flag_d BOOL NOT NULL DEFAULT FALSE,
flag_i BOOL NOT NULL DEFAULT TRUE,
flag_z BOOL NOT NULL DEFAULT FALSE,
flag_c BOOL NOT NULL DEFAULT FALSE
);
Nota los CHECK constraints: el acumulador no puede valer más de 255. El program counter está limitado a 16 bits. Son las restricciones de hardware impuestas por el propio engine de la base de datos.
La memoria es una tabla de 65536 filas, una por byte:
CREATE TABLE pg6502.mem (
addr INT PRIMARY KEY CHECK (addr BETWEEN 0 AND 65535),
val INT NOT NULL CHECK (val BETWEEN 0 AND 255)
);
Leer un byte que no existe devuelve 0, simulando el comportamiento “open bus” del hardware real. Las lecturas de 16 bits little-endian son una función que concatena dos lecturas de 8 bits:
CREATE OR REPLACE FUNCTION pg6502.mem_read16(p_addr INT)
RETURNS INT AS $$
BEGIN
RETURN pg6502.mem_read(p_addr)
+ (pg6502.mem_read(p_addr + 1) * 256);
END
$$ LANGUAGE plpgsql;
Modos de direccionamiento como funciones
El 6502 tiene modos de direccionamiento peculiares como “indirect indexed” ((addr),Y) o “indexed indirect” ((addr,X)), que son la pesadilla de todo principiante pero la alegría de todo ensamblador experimentado. En pg_6502, cada uno es una función:
-- Indirect Indexed: lee un puntero de la zero page, le suma Y
CREATE OR REPLACE FUNCTION pg6502.addr_indirect_y(p_pc INT)
RETURNS INT AS $$
DECLARE
v_y INT;
v_ptr INT;
BEGIN
SELECT y INTO v_y FROM pg6502.cpu;
v_ptr := pg6502.mem_read16(pg6502.mem_read(p_pc + 1));
RETURN v_ptr + v_y;
END
$$ LANGUAGE plpgsql;
Ese SELECT y INTO v_y FROM pg6502.cpu me parece bellísimo. Estás literalmente leyendo un registro del procesador con una consulta SQL.
El bucle fetch-decode-execute
El corazón de cualquier emulador es el bucle de ejecución. Aquí está el de pg_6502:
CREATE OR REPLACE FUNCTION pg6502.run(
p_max_cycles INT DEFAULT 100000,
p_log_interval INT DEFAULT 1000000
)
RETURNS INT AS $$
DECLARE
v_result TEXT;
v_cycles INT := 0;
v_pc INT;
BEGIN
LOOP
v_result := pg6502.execute_instruction();
v_cycles := v_cycles + 1;
IF v_cycles % p_log_interval = 0 THEN
SELECT pc INTO v_pc FROM pg6502.cpu;
RAISE NOTICE 'Cycles: %, PC: %', v_cycles, v_pc;
END IF;
EXIT WHEN v_result = 'BRK';
EXIT WHEN v_cycles >= p_max_cycles;
END LOOP;
RETURN v_cycles;
END
$$ LANGUAGE plpgsql;
Cada instrucción modifica la tabla cpu directamente. Un LDA #$42 hace un UPDATE pg6502.cpu SET a = 66, pc = .... El estado del procesador es literalmente una fila que se reescribe ciclo a ciclo. Es como ver un circuito digital donde las señales son UPDATEs en vez de voltajes.
Y lo más alucinante: el proyecto incluye un ensamblador escrito también en SQL. La función pg6502.assemble() parsea texto como LDA #$05 y devuelve binario listo para cargar. Dentro de Postgres. Qué época para estar vivo.
El bug del indirect en hardware real (y cómo pg_6502 lo implementa)
El MOS 6502 real tiene un bug documentado: la instrucción JMP ($xxFF) no cruza el límite de página. Si el puntero está en $10FF, lee el byte alto desde $1000 en vez de $1100. Es un comportamiento tan icónico que hasta Nintendo lo aprovechó en algunos juegos.
Mirando la función addr_indirect de pg_6502, usa mem_read16 que sí cruza páginas correctamente:
CREATE OR REPLACE FUNCTION pg6502.addr_indirect(p_pc INT)
RETURNS INT AS $$
DECLARE
v_ptr INT;
BEGIN
v_ptr := pg6502.mem_read16(p_pc + 1);
RETURN pg6502.mem_read16(v_ptr);
END
$$ LANGUAGE plpgsql;
Esto significa que pg_6502 no replica ese bug particular del hardware. Es una decisión razonable para un emulador didáctico, pero los puristas lo notarán.
Cómo ejecutarlo tú mismo
Clonas el repo, levantas Docker, y en segundos estás ejecutando código 6502 dentro de una base de datos relacional:
git clone https://github.com/lasect/pg_6502.git
cd pg_6502
docker compose up -d
make reset # carga schema + binario de prueba
make test # ejecuta Klaus 6502 Functional Test
Y si quieres ver el estado del CPU:
SELECT * FROM pg6502.state;
Te devuelve algo como:
a | x | y | sp | pc | pc_hex | flags
---+---+---+----+-----+--------+------
0 | 5 | 0 | ff | 1536| 0600 | ··B·I··
Las flags representadas como caracteres: N Negative, V Overflow, B Break, D Decimal, I Interrupt, Z Zero, C Carry. Tan limpio como un panel de LEDs en un front panel de los 70.
Mi veredicto
pg_6502 no pretende ser práctico. Es arte computacional. Es la clase de proyecto que te recuerda por qué te gustaba programar: por la pura alegría de ver si se puede hacer.
Pero más allá de la anécdota, hay una lección seria. Este emulador demuestra que SQL —específicamente PL/pgSQL— es Turing-completo de una forma tan explícita que puedes ejecutar cualquier programa de 8 bits dentro de él. Cada UPDATE es un ciclo de reloj. Cada SELECT es una lectura de bus. Las CHECK constraints son las leyes de la física del chip.
Si quieres entender cómo funciona un procesador por dentro, leer el código de pg_6502 es una experiencia más intuitiva que muchos libros de texto. Las tablas sustituyen a los circuitos, las funciones sustituyen a las puertas lógicas, y el resultado es un 6502 que corre dentro de algo que pensabas que solo servía para guardar usuarios y contraseñas.
32 estrellas en GitHub para algo que merece mil. Ve a lasect/pg_6502 y regálale una.