add turingcomplete cpu compiler
This commit is contained in:
312
docs/todo.md
Normal file
312
docs/todo.md
Normal 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.
|
||||
Reference in New Issue
Block a user