313 lines
9.8 KiB
Markdown
313 lines
9.8 KiB
Markdown
# 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.
|