add turingcomplete cpu compiler

This commit is contained in:
Jose Luis Montañes Ojados
2026-03-03 01:18:47 +01:00
parent d8b4f9b2ea
commit 9610957f1b
13 changed files with 1284 additions and 6 deletions

312
docs/todo.md Normal file
View File

@@ -0,0 +1,312 @@
# TODO: Backend mycpu_v2 para j-lang
Roadmap de implementación del generador de código para el CPU v2 de 16-bit.
El objetivo es que `gencode.h` tome el AST del frontend y produzca:
- **Texto ensamblador** legible (para debug)
- **Binario** (array de bytes para cargar en PROM)
---
## Referencia rápida del CPU v2
```
Instrucción: 8 bytes = [OPCODE:16][PARAM1:16][PARAM2:16][TARGET:16]
PC cuenta en words de 16-bit → instrucción N está en PC = N × 4
Registros libres: REG0-REG11 (12 registros, 16 bits cada uno)
Registros especiales: REG12(RAM_VAL), REG13(RAM_ADDR), REG14(PC), REG15(I/O)
Modos de direccionamiento (codificados en el opcode):
base + 0x00 = registro, registro
base + 0x40 = inmediato, registro
base + 0x80 = registro, inmediato
base + 0xC0 = inmediato, inmediato
RAM:
Leer: Escribir dirección en REG13 → REG12 se actualiza automáticamente
Escribir: REG12 = valor, REG13 = addr → RSTR (0x18)
```
### Pseudo-instrucciones útiles
```asm
MOV #valor, REGn → ADD #valor, #0, REGn (opcode 0xC0)
MOV REGa, REGb ADD REGa, #0, REGb (opcode 0x80)
JMP #addr → EQ #0, #0, addr (opcode 0xD0, siempre true)
NOP ADD #0, #0, REG0 (opcode 0xC0)
```
---
## Convención de registros
```
REG0-REG3 → Temporales para evaluación de expresiones (expression stack)
REG4-REG5 → Auxiliares (spill de expresiones profundas)
REG6-REG11 → Libres / reserva futura (frame pointer, etc.)
REG12 → RAM VALUE (especial, no tocar directamente)
REG13 → RAM ADDR (especial, no tocar directamente)
REG14 → PC (especial)
REG15 → I/O (especial)
```
## Almacenamiento de variables
Todas las variables en **RAM**. Tabla nombre→dirección en tiempo de compilación.
```asm
; Leer variable 'x' (dirección addr) → REGn
ADD #addr, #0, REG13 ; REG13 = addr
ADD REG12, #0, REGn ; REGn = RAM[addr] (lectura automática)
; Escribir variable 'x' (valor en REGn, dirección addr)
ADD #addr, #0, REG13 ; REG13 = addr
ADD REGn, #0, REG12 ; REG12 = valor
RSTR ; RAM[addr] = REG12
```
---
## Fase 0: Infraestructura del emisor
**Objetivo**: Estructuras y funciones base para emitir instrucciones.
- [ ] Struct `Instruction` (opcode, param1, param2, target)
- [ ] Buffer de instrucciones (array dinámico donde se acumulan)
- [ ] Función `emit(opcode, p1, p2, target)` — agrega instrucción al buffer
- [ ] Tabla de variables: mapeo nombre→dirección_RAM (compilación)
- [ ] Función `lookupOrCreateVar(name)` — busca o asigna dirección RAM
- [ ] Sistema de labels/backpatching:
- [ ] `emitPlaceholder()` → emite instrucción con target=0, retorna índice
- [ ] `patchTarget(index, target)` → rellena el target de instrucción emitida
- [ ] `currentAddr()` → posición actual (nº de instrucción)
- [ ] Output ASM: recorrer buffer → texto legible con mnemonicos
- [ ] Output binario: recorrer buffer → array de bytes (8 bytes/instrucción)
**Criterio**: Emitir instrucciones hardcoded, ver texto ASM y binario generados.
---
## Fase 1: Constantes, asignaciones y print
**Objetivo**: Compilar `x = 42` y `print x`.
- [ ] Compilar `NODE_INT_LIT` → cargar inmediato en REG[depth]
```asm
ADD #42, #0, REG0 ; MOV #42, REG0
```
- [ ] Compilar `NODE_ASSIGN` → evaluar expr → REG0, store en RAM
```asm
; (resultado ya en REG0)
ADD #addr, #0, REG13 ; REG13 = dirección de variable
ADD REG0, #0, REG12 ; REG12 = valor
RSTR ; RAM[addr] = valor
```
- [ ] Compilar `NODE_VAR` → leer de RAM a REG[depth]
```asm
ADD #addr, #0, REG13 ; REG13 = dirección
ADD REG12, #0, REG0 ; REG0 = RAM[addr]
```
- [ ] Compilar `NODE_PRINT` → evaluar expr → REG0, copiar a I/O
```asm
ADD REG0, #0, REG15 ; OUTPUT = REG0
```
- [ ] Compilar `NODE_BLOCK` → iterar y compilar cada statement
**Test**: `simple.j` (x = 10). `print 10` → escribe 10 en REG15.
---
## Fase 2: Expresiones aritméticas
**Objetivo**: Compilar `x = 10 + 20 * 3`.
**Estrategia**: Register depth counter. Cada sub-expresión deposita resultado en `REG[depth]`.
- [ ] Variable `int reg_depth = 0` para tracking
- [ ] Compilar `NODE_BINOP`:
```
compilar left → resultado en REG[depth]
depth++
compilar right → resultado en REG[depth]
depth--
emit OP REG[depth], REG[depth+1], REG[depth]
```
- [ ] Manejar profundidad > 4 → PUSH/POP al stack (spill)
- [ ] Mapeo de operadores:
- `+` → ADD (0x00)
- `-` → SUB (0x01)
- `*` → MUL (0x02)
- `/` → DIV (0x03)
**Test**: `sum.j`, `resta.j`. Verificar que `2 + 3 * 4` da 14.
---
## Fase 3: Comparaciones y control de flujo
**Objetivo**: Compilar `if` y `while`.
### if
- [ ] Compilar `NODE_IF`:
```
compilar condición left → REG0
compilar condición right → REG1
emit CONDICIONAL_INVERSO REG0, REG1, [placeholder]
compilar bloque then
patch placeholder → currentAddr() × 4
```
- [ ] Mapeo de condicionales **inversos** (saltar si la condición es FALSA):
- `==` en AST → emit `NEQ` (0x11)
- `!=` en AST → emit `EQ` (0x10)
- `<` en AST → emit `GRE` (0x15) — saltar si >=
- `>` en AST → emit `LSE` (0x13) — saltar si <=
### while
- [ ] Compilar `NODE_WHILE`:
```
loop_start = currentAddr()
compilar condición left → REG0
compilar condición right → REG1
emit CONDICIONAL_INVERSO REG0, REG1, [placeholder_exit]
compilar cuerpo
emit EQ #0, #0, (loop_start × 4) ; JMP incondicional
patch placeholder_exit → currentAddr() × 4
```
### Recordar
- **PC = instrucción_index × 4** (cada instrucción = 4 words de 16-bit)
- El salto incondicional es `EQ #0, #0, target` (0xD0, siempre true)
**Test**: `if.j`, `while.j`. While que cuenta de 0 a 10.
---
## Fase 4: Funciones (CALL/RET)
**Objetivo**: Compilar `fn` definitions y llamadas.
### Convención de llamada
```
1. Caller pushea argumentos al stack (derecha a izquierda)
2. Caller ejecuta CALL #dirección (pushea PC+1 al stack, salta)
3. Callee popea argumentos → variables locales en RAM
4. Callee ejecuta cuerpo
5. Callee deja resultado en REG0
6. Callee ejecuta RET (popea PC del stack, salta)
7. Caller usa REG0 como valor de retorno
```
### Tareas
- [ ] Compilar `NODE_FN_DEF`:
```
emit JMP [placeholder_skip] ; saltar sobre el cuerpo
fn_addr = currentAddr()
para cada param (de derecha a izq):
emit POP → REGn
store REGn → RAM[param_addr]
compilar cuerpo
emit RET
patch placeholder_skip → currentAddr() × 4
registrar fn_name → fn_addr en tabla de funciones
```
- [ ] Compilar `NODE_CALL`:
```
para cada argumento (de izq a der):
compilar argumento → REG0
emit PUSH REG0
emit CALL #(fn_addr × 4)
; resultado queda en REG0
```
- [ ] Compilar `NODE_RETURN`:
```
compilar expresión → REG0
emit RET
```
- [ ] Resolver scope de variables locales:
- **Opción simple**: cada función tiene su propio rango de RAM
- **Opción avanzada**: frame pointer (registro base + offset para locales)
**Test**: `functions.j`, `custom_fn.j`.
---
## Fase 5: Strings y objetos (avanzado)
**Objetivo**: Soportar strings, clases, campos e instancias.
- [ ] Strings en RAM — caracteres consecutivos, variable apunta a dirección base
- [ ] Print de strings — loop: leer cada char de RAM → escribir en REG15
- [ ] Instancias — bloque de RAM con campos, variable apunta a base
- [ ] Campos — offset fijo desde base de instancia
- [ ] Métodos — funciones con `self` (dirección de instancia) como primer arg
- [ ] Constructor — reservar espacio en RAM, llamar a `init`
**Nota**: Requiere un allocator en runtime para reservar memoria dinámica en RAM.
### Estrategia de allocator en runtime
Hay dos opciones, de menor a mayor complejidad:
**Opción A: Bump allocator (recomendado para empezar)**
La más simple. Una dirección de RAM fija (ej: `RAM[0x00FF]`) actúa como "heap pointer" que empieza al final de las variables estáticas. Cada asignación avanza el pointer. No tiene `free`.
```asm
; alloc(size) — size en REG1, retorna dirección en REG0
ADD #0x00FF, #0, REG13 ; REG13 = dirección del heap_ptr
ADD REG12, #0, REG0 ; REG0 = heap_ptr actual (dirección a retornar)
ADD REG12, REG1, REG12 ; REG12 = heap_ptr + size
RSTR ; guardar nuevo heap_ptr en RAM[0x00FF]
; REG0 = dirección del bloque asignado
```
~4 instrucciones. Suficiente para strings literales y concatenaciones simples.
**Opción B: Allocator con metadata (como `allocator.h`, pero en ASM del CPU v2)**
Mismo diseño conceptual que `src/memory/allocator.h` pero implementado como rutina en ensamblador del CPU v2:
- Cada bloque en RAM: `[size:16][in_use:16][payload...]`
- Loop que recorre bloques con comparaciones + saltos (first-fit)
- `free` marca `in_use = 0`
- ~30-50 instrucciones del CPU v2
Solo necesario si se van a liberar strings (reasignar variables string, concatenaciones temporales).
**Recomendación**: Empezar con bump allocator. Si más adelante se necesita `free`, implementar opción B usando el diseño de `allocator.h` como referencia conceptual.
**Test**: `str.j`, `classes.j`.
---
## Diagrama de dependencias
```
Fase 0 (infraestructura)
└── Fase 1 (constantes, asignación, print)
└── Fase 2 (aritmética)
└── Fase 3 (if/while)
└── Fase 4 (funciones)
└── Fase 5 (strings/objetos)
```
## Verificación por fase
| Fase | Archivos de test |
|------|-----------------|
| 1 | `simple.j` |
| 2 | `sum.j`, `resta.j` |
| 3 | `if.j`, `while.j` |
| 4 | `functions.j`, `custom_fn.j` |
| 5 | `str.j`, `classes.j` |
Comparar output generado (ASM + binario) con lo que la VM produce para la misma entrada.